package com.heima.leadnews.wemedia.service.impl;

import com.alibaba.fastjson.JSON;
import com.baidu.aip.ocr.AipOcr;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hankcs.algorithm.AhoCorasickDoubleArrayTrie;
import com.heima.feign.article.ArticleFeign;
import com.heima.green.scan.GreenImageScan;
import com.heima.green.scan.GreenTextScan;
import com.heima.leadnews.wemedia.mapper.WmNewsMapper;
import com.heima.leadnews.wemedia.service.*;
import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.wemedia.dto.WmNewsDTO;
import com.heima.model.wemedia.dto.WmNewsDownOrUpDTO;
import com.heima.model.wemedia.dto.WmNewsPageDTO;
import com.heima.model.wemedia.dto.WmNewsSubmitDTO;
import com.heima.model.wemedia.pojo.*;
import com.heima.utils.common.UserContext;
import com.jlefebure.spring.boot.minio.MinioException;
import com.jlefebure.spring.boot.minio.MinioService;
import com.pojo.ScanResult;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import okhttp3.internal.tls.OkHostnameVerifier;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.entity.ContentType;
import org.json.JSONArray;
import org.json.JSONObject;
//import org.springframework.amqp.core.Message;
//import org.springframework.amqp.core.MessageBuilder;
//import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 自媒体图文内容信息表 服务实现类
 * </p>
 *
 * @author author
 * @since 2023-06-03
 */
@Service
@Slf4j
public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {

    @Autowired
    private WmNewsMaterialService wmNewsMaterialService;

    @Autowired
    private WmMaterialService wmMaterialService;

    @Autowired
    private GreenTextScan greenTextScan;

    @Autowired
    private GreenImageScan greenImageScan;

    @Autowired
    private MinioService minioService;

    @Autowired
    private ArticleFeign articleFeign;

    @Autowired
    private WmUserService wmUserService;

    @Autowired
    private WmChannelService wmChannelService;

    @Value("${spring.minio.url}")
    private String minioUrl;

    @Value("${spring.minio.bucket}")
    private String bucket;

    @Autowired
    private Configuration configuration;

    @Autowired
    private WmSensitiveService wmSensitiveService;

    @Autowired
    private AipOcr aipOcr;

    @Autowired
    private KafkaTemplate kafkaTemplate;
//    @Autowired
//    private RabbitTemplate rabbitTemplate;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 条件分页查询频道信息
     *
     * @return
     */
    @Override
    public ResponseResult pager(WmNewsPageDTO dto) {
        dto.checkParam();
        //根据条件分页查询
        IPage<WmNews> page = new Page<>(dto.getPage(), dto.getSize());
        LambdaQueryWrapper<WmNews> queryWrapper = new LambdaQueryWrapper<>();
        //设置查询条件
        //搜索关键字不为空
        if (StringUtils.isNotEmpty(dto.getKeyword())) {
            queryWrapper.like(WmNews::getTitle, dto.getKeyword());
        }
        //搜索频道id不为空
        if (dto.getChannelId() != null) {
            queryWrapper.eq(WmNews::getChannelId, dto.getChannelId());
        }
        //搜索状态不为空
        if (dto.getStatus() != null) {
            queryWrapper.eq(WmNews::getStatus, dto.getStatus());
        }
        //搜索时间段不为空
        if (dto.getBeginPubdate() != null && dto.getEndPubdate() != null) {
            queryWrapper.between(WmNews::getPublishTime, dto.getBeginPubdate(), dto.getEndPubdate());
        }
        queryWrapper.eq(WmNews::getUserId, UserContext.getUserId());
        //根据时间倒序排列
        queryWrapper.orderByDesc(WmNews::getPublishTime);
        page(page, queryWrapper);
        long total = page.getTotal();
        List<WmNews> records = page.getRecords();
        return PageResponseResult.okResult(dto.getPage(), dto.getSize(), total, records);
    }

    /**
     * 添加/修改自媒体文章信息
     *
     * @param dto
     * @return
     */
    @Override
    @GlobalTransactional
    public ResponseResult submit(WmNewsSubmitDTO dto) throws Exception {
        WmNews wmNews = new WmNews();
        BeanUtils.copyProperties(dto, wmNews);
        //补充属性
        wmNews.setUserId(UserContext.getUserId().intValue());
        wmNews.setCreatedTime(LocalDateTime.now());
        //如果用户选择的自动，需要从文章中拿到封面图
        String images = dto.getImages();
        List<String> imageList = JSON.parseArray(images, String.class);
        String imageString = StringUtils.join(imageList, ",");
        //获取文章中所有图片
        List<String> contentImages = getContentImages(dto.getContent());
        //获取封面图片
        List<String> coverImages = getCoverImages(contentImages);
        if (dto.getType() == WmNews.Type.AUTO) {
            imageString = StringUtils.join(coverImages, ",");
            wmNews.setType(coverImages.size());
        }
        wmNews.setImages(imageString);
        wmNews.setPublishTime(dto.getPublishTime());
        if (dto.getId() == null) {
            save(wmNews);
        } else {
            updateById(wmNews);
        }
        //3.如果用户点击的是提交审核，需要在wm_news_material中间表存储材料和文章关系表
        if (wmNews.getStatus() == WmNews.Status.SUBMIT) {
            if (dto.getId() != null) {
                //修改操作,先删除中间表关系表wm_news_material，文章和素材的关系
                LambdaQueryWrapper<WmNewsMaterial> updateQueryWrapper = new LambdaQueryWrapper<>();
                updateQueryWrapper.eq(WmNewsMaterial::getNewsId, dto.getId());
                wmNewsMaterialService.remove(updateQueryWrapper);
            }
            //3.1 mp在保存后，会自动将id封装回wmNews对象
            //获取文章id
            Integer wmNewsId = wmNews.getId();
            //根据素材图片的url查询素材的id
            saveNewsMaterialRelation(contentImages, coverImages, wmNewsId);
        }
        //审核文章
        autoScan(wmNews);
        return ResponseResult.okResult();
    }

    /**
     * 保存文章和封面图片，文章内容和图片的映射关系
     * 自定义抽取的方法
     *
     * @param contentImages
     * @param coverImages
     * @param wmNewsId
     */
    private void saveNewsMaterialRelation(List<String> contentImages, List<String> coverImages, Integer wmNewsId) {
        LambdaQueryWrapper<WmMaterial> imageQueryWrapper = new LambdaQueryWrapper<>();
        imageQueryWrapper.in(WmMaterial::getUrl, contentImages);
        List<WmMaterial> contentImageList = wmMaterialService.list(imageQueryWrapper);
        for (WmMaterial wmMaterial : contentImageList) {
            WmNewsMaterial wmNewsMaterial = new WmNewsMaterial();
            wmNewsMaterial.setNewsId(wmNewsId);
            wmNewsMaterial.setMaterialId(wmMaterial.getId());
            wmNewsMaterial.setType(Boolean.FALSE);
            //保存到中间表
            wmNewsMaterialService.save(wmNewsMaterial);
        }
        //根据素材 封面  的url查询素材的id
        LambdaQueryWrapper<WmMaterial> coverQueryWrapper = new LambdaQueryWrapper<>();
        imageQueryWrapper.in(WmMaterial::getUrl, coverImages);
        List<WmMaterial> coverImagesList = wmMaterialService.list(coverQueryWrapper);
        for (WmMaterial wmMaterial : coverImagesList) {
            WmNewsMaterial wmNewsMaterial = new WmNewsMaterial();
            wmNewsMaterial.setNewsId(wmNewsId);
            wmNewsMaterial.setMaterialId(wmMaterial.getId());
            wmNewsMaterial.setType(Boolean.TRUE);
            //保存到中间表
            wmNewsMaterialService.save(wmNewsMaterial);
        }
    }

    /**
     * 根据id查询文章
     *
     * @param id
     * @return
     */
    @Override
    public ResponseResult findById(Integer id) {
        WmNews wmNews = getById(id);
        if (wmNews == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }
        if (wmNews.getUserId().longValue() != UserContext.getUserId()) {
            return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
        }
        return ResponseResult.okResult(wmNews);
    }

    /**
     * 根据id删除文章
     *
     * @param id
     * @return
     */
    @Override
    @Transactional
    public ResponseResult deleteById(Integer id) {
        //1.根据文章id查询文章
        WmNews wmNews = getById(id);
        //2.健壮性判断
        if (wmNews == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }
        //3.文章状态参数合法性判断,已经发布的文章不能删除
        if (wmNews.getStatus() == WmNews.Status.PUBLISHED) {
            return ResponseResult.errorResult(AppHttpCodeEnum.NEED_ADMIND);
        }
        //4.只能删除自己的文章
        if (wmNews.getUserId().longValue() != UserContext.getUserId()) {
            return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
        }
        //5.删除文章
        removeById(id);
        //6.删除文章材料关系表
        LambdaQueryWrapper<WmNewsMaterial> updateQueryWrapper = new LambdaQueryWrapper<>();
        updateQueryWrapper.eq(WmNewsMaterial::getNewsId, wmNews.getId());
        wmNewsMaterialService.remove(updateQueryWrapper);
        return ResponseResult.okResult();
    }

    /**
     * 自动审核文章
     *
     * @param wmNews
     * @return
     */
    @Override
    @Transactional
    public void autoScan(WmNews wmNews) throws Exception {
        // 3.1.获取图片url(内容图片)
        List<String> contentImages = getContentImages(wmNews.getContent());
        //3.2.获取图片url(封面图片)
        String[] split = wmNews.getImages().split(",");
        List<String> coverList = new ArrayList<>();
        Collections.addAll(coverList, split);
        //3.3 调用百度OCR获取图片中的文字，封面图片中的文字，文章内容图片中的文字
        String contentInImages = getContentInImages(contentImages);
        String contentInCovers = getContentInImages(coverList);
        // 1.获取文章和图片中的文本
        String contentText = getText(wmNews) + contentInImages + contentInCovers;
        // 1.1 文本审核的时候，先基于自己的敏感词表审核，审核通过向阿里云审核
        List sensitiveWords = isExistSensitiveWords(contentText);
        if (sensitiveWords.size() > 0) {
            // 1.2 在自建库中找到敏感词，修改文章状态
            wmNews.setStatus(WmNews.Status.AUDIT_FAILED);
            wmNews.setReason("文章的文本中在自建库中发现了违规信息" + sensitiveWords);
            //修改文章信息 状态和审核原因，结束方法
            updateById(wmNews);
            return;
        }
        // 2.调用阿里云内容安全服务审核文本
        ScanResult scanResult = greenTextScan.greeTextScan(contentText);
        // 2.1获取到文本的审核结果
        String suggestion = scanResult.getSuggestion();
        // 2.2 解析文本的审核结果
        if ("block".equals(suggestion)) {
            // 2.3  审核未通过，修改自媒体文章状态为 2 ;  2 审核失败
            wmNews.setStatus(WmNews.Status.AUDIT_FAILED);
            wmNews.setReason("文章的文本中包含违规的内容" + scanResult.getLabel());
            //修改文章信息 状态和审核原因，结束方法
            updateById(wmNews);
            return;
        }
        // 3.审核文本通过:--->审核图片
        // 3.4 调用阿里云审核文章内的图片
        reviewImages(wmNews, contentImages);
        // 3.5 审核阿里云封面的图片
        reviewImages(wmNews, coverList);
        // 4.审核通过
        // 4.1判断是否要延迟发布
        if (wmNews.getPublishTime().after(new Date())) {
            // 4.2.是:修改自媒体文章状态为8;
            wmNews.setStatus(WmNews.Status.APPROVED);
            wmNews.setReason("文章审核通过待发布");
            //4.3 修改文章信息 状态和审核原因，结束方法
            //发送消息到rabbitmq 文章的id 方便后续自动发布
//            long ttl = wmNews.getPublishTime().getTime() - System.currentTimeMillis();
//            Message message = MessageBuilder.withBody(String.valueOf(wmNews.getId()).getBytes())
//                    .setExpiration(String.valueOf(ttl))
//                    .build();
//            rabbitTemplate.convertAndSend("direct", "blue", message);
            redisTemplate.opsForZSet().add("wmnews", String.valueOf(wmNews.getId()), wmNews.getPublishTime().getTime());
        } else {
            // 4.4 否:
            // 4.4.远程调用文章微服务，保存用户端文章信息
            publishNews(wmNews);
        }
        updateById(wmNews);
    }

    /**
     * 发布文章到article微服务
     *
     * @param wmNews
     * @throws IOException
     * @throws TemplateException
     * @throws MinioException
     */
    private void publishNews(WmNews wmNews) throws IOException, TemplateException, MinioException {
        Long articleId = saveArticle(wmNews);
        String staticHtml = getStaticHtml(wmNews.getContent());
        //4.6.更新详情页面地址到用户端文章表
        articleFeign.updateStaticUrl(articleId, staticHtml);
        //4.7 修改文章信息 状态和审核原因，结束方法
        wmNews.setStatus(WmNews.Status.PUBLISHED);
        wmNews.setSubmitedTime(LocalDateTime.now());
        wmNews.setArticleId(articleId);
        wmNews.setReason("文章审核通过,已经发布");
        updateById(wmNews);
    }

    /**
     * 查询自媒体文章表中状态为 8 ,并且发布时间小于当前时间的文章
     *
     * @param
     * @return
     */
    @Override
    public ResponseResult<List<WmNews>> findEnableNews() {
        LambdaQueryWrapper<WmNews> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(WmNews::getStatus, WmNews.Status.APPROVED);
        queryWrapper.le(WmNews::getPublishTime, new Date());
        List<WmNews> list = list(queryWrapper);
        return ResponseResult.okResult(list);
    }

    /**
     * 发布定时任务检测到的需要发布的文章
     *
     * @param wmNews
     * @return
     * @throws TemplateException
     * @throws MinioException
     * @throws IOException
     */
    @Override
    public ResponseResult publish(WmNews wmNews) throws TemplateException, MinioException, IOException {
        publishNews(wmNews);
        return ResponseResult.okResult();
    }

    /**
     * 根据id查询文章,忽略登陆人id验证，内部调用的接口
     * 供给发布文章微服务调用
     *
     * @param id
     * @return
     */
    @Override
    public ResponseResult<WmNews> getNewsId(Integer id) {
        return (getById(id) != null) ? ResponseResult.okResult(getById(id)) : null;
    }

    /**
     * 自媒体文章的上下架。
     *
     * @param
     * @return
     */
    @Override
    public ResponseResult downOrUp(WmNewsDownOrUpDTO dto) {
        //获取到文章对象
        WmNews wmNews = getById(dto.getId());
        //健壮性判断
        if (wmNews == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }
        //判断文章状态是否为9，只有发布了的文章才可以进行上下架操作
        if (wmNews.getStatus() != WmNews.Status.PUBLISHED) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        //登陆人信息判断，只能对自己的文章进行上下架操作
        if (wmNews.getUserId() != UserContext.getUserId().intValue()) {
            return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH);
        }
        //修改文章的上下架状态
        wmNews.setEnable(dto.getEnable());
        //更新文章数据库信息
        updateById(wmNews);
        //发送消息到kafka，用户端文章的基本信息id
        String articleId = wmNews.getArticleId().toString();
        String isEnable = dto.getEnable().toString();
        kafkaTemplate.send(WmNewsDownOrUpDTO.NEWS_DOWN_UP,articleId,isEnable);
        return ResponseResult.okResult();
    }

    /**
     * 基于自己的敏感词表审核,通过返回true 不通过返回false
     *
     * @param context
     * @return
     */
    private List isExistSensitiveWords(String context) {
        TreeMap<String, String> map = new TreeMap<>();
        LambdaQueryWrapper<WmSensitive> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.select(WmSensitive::getSensitives);
        List<WmSensitive> list = wmSensitiveService.list(queryWrapper);
        for (WmSensitive wmSensitive : list) {
            map.put(wmSensitive.getSensitives(), wmSensitive.getSensitives());
        }
        // Build an AhoCorasickDoubleArrayTrie
        AhoCorasickDoubleArrayTrie<String> acdat = new AhoCorasickDoubleArrayTrie<String>();
        acdat.build(map);
        // Test it
        List<AhoCorasickDoubleArrayTrie.Hit<String>> wordList = acdat.parseText(context);
        log.info("在自建敏感词库中发现违规信息，参数为：{}", wordList);
        return wordList;
    }

    /**
     * 保存文章的静态页面到 minio 中
     *
     * @param content
     * @throws IOException
     * @throws TemplateException
     * @throws MinioException
     */
    private String getStaticHtml(String content) throws IOException, TemplateException, MinioException {
        //1.1 编写模板.
        //1.2 准备数据(文章内容).
        Writer writer = null;
        InputStream is = null;
        File file = null;
        try {
            List<ContentSegment> data = JSON.parseArray(content, ContentSegment.class);
            //1.3 模板＋数据--->API---->静态页面
            Template template = configuration.getTemplate("article_content.ftl");
            Map<String, Object> map = new HashMap<>();
            map.put("data", data);
            String fileName = UUID.randomUUID().toString() + ".html";
            String home = System.getProperty("user.home");
            file = new File(home + "\\" + fileName);
            writer = new FileWriter(file);
            template.process(map, writer);
            //将详情页面保存到minio
            is = new FileInputStream(file);
            minioService.upload(Paths.get(fileName), is, ContentType.TEXT_HTML);
            String staticHtml = minioUrl + "/" + bucket + "/" + fileName;
            return staticHtml;
        } finally {
            if (writer != null) {
                writer.close();
            }
            if (is != null) {
                is.close();
            }
            if (file != null) {
                file.delete();
            }
        }
    }

    /**
     * 远程调用文章微服务，完成用户端文章信息保存
     *
     * @param wmNews
     */
    private Long saveArticle(WmNews wmNews) {
        WmNewsDTO dto = new WmNewsDTO();
        BeanUtils.copyProperties(wmNews, dto);
        WmUser wmUser = wmUserService.getById(wmNews.getUserId());
        WmChannel wmChannel = wmChannelService.getById(wmNews.getChannelId());
        dto.setAuthorName(wmUser.getName());
        dto.setChannelName(wmChannel.getName());
        ResponseResult<Long> article = articleFeign.saveArticle(dto);
        Long articleId = article.getData();
        return articleId;
    }

    /**
     * 自动审核文章图片的方法
     *
     * @param wmNews
     * @param images
     * @throws Exception
     */
    private void reviewImages(WmNews wmNews, List<String> images) throws Exception {
        for (String contentImage : images) {
            // 4.从minio中下载图片
            String name = FilenameUtils.getName(contentImage);
            InputStream is = minioService.get(Paths.get(name));
            // 5.调用阿里云内容安全服务审核图片
            ScanResult imageScanResult = greenImageScan.imageScan(is);
            //5.1解析审核结果
            String imageSuggestion = imageScanResult.getSuggestion();
            if ("block".equals(imageSuggestion)) {
                // 5.2  审核未通过，修改自媒体文章状态为 2 ;  2 审核失败
                wmNews.setStatus(WmNews.Status.AUDIT_FAILED);
                wmNews.setReason("文章的图片中包含违规的内容" + imageScanResult.getLabel());
                //5.3 修改文章信息 状态和审核原因，结束方法
                updateById(wmNews);
                return;
            }
            if ("review".equals(imageSuggestion)) {
                // 5.2  审核未通过，修改自媒体文章状态为 3 ;  3 人工审核
                wmNews.setStatus(WmNews.Status.HUMAN_REVIEW);
                wmNews.setReason("文章的图片中包含违规的内容" + imageScanResult.getLabel());
                // 5.3 修改文章信息 状态和审核原因，结束方法
                updateById(wmNews);
                return;
            }
        }
    }

    /**
     * 获取文章图片中的文字的方法
     *
     * @param images
     * @param images
     * @throws Exception
     */
    private String getContentInImages(List<String> images) throws Exception {
        StringBuilder stringBuilder = new StringBuilder();
        for (String contentImage : images) {
            // 4.从minio中下载图片
            String name = FilenameUtils.getName(contentImage);
            InputStream is = minioService.get(Paths.get(name));
            byte[] bytes = IOUtils.toByteArray(is);
            JSONObject result = aipOcr.basicGeneral(bytes, new HashMap<>());
            int num = result.getInt("words_result_num");
            //如果图片中没有文字，返回null，结束方法
            if (num <= 0) {
                return null;
            }
            //解析返回结果，获取图片中的文字
            JSONArray jsonArray = result.getJSONArray("words_result");
            for (Object o : jsonArray) {
                JSONObject word = (JSONObject) o;
                stringBuilder.append(word.getString("words"));
            }
        }
        return stringBuilder.toString();
    }

    /**
     * 获取文章中的文本信息
     * 自定义抽取的方法
     *
     * @param wmNews
     * @return
     */
    private String getText(WmNews wmNews) {
        String content = wmNews.getContent();
        List<ContentSegment> contentSegments = JSON.parseArray(content, ContentSegment.class);
        StringBuilder stringBuilder = new StringBuilder();
        for (ContentSegment contentSegment : contentSegments) {
            String type = contentSegment.getType();
            String value = contentSegment.getValue();
            if (type.equals("text")) {
                stringBuilder.append(value);
            }
        }
        String contentText = stringBuilder.toString();
        return contentText;
    }

    /**
     * 获取文章中图片的地址
     *
     * @param content
     * @return
     */
    private List<String> getContentImages(String content) {
        List<ContentSegment> segmentList = JSON.parseArray(content, ContentSegment.class);
        List<String> ContentImages = new ArrayList<>();
        for (ContentSegment segment : segmentList) {
            if (segment.getType().equals("image")) {
                ContentImages.add(segment.getValue());
            }
        }
        return ContentImages;
    }

    /**
     * 从内容中获取图片,内容中所有的图片
     *
     * @param contentImages
     * @return
     */
    private List<String> getCoverImages(List<String> contentImages) {
        if (contentImages.isEmpty()) {
            return contentImages;
        }
        if (contentImages.size() > 3) {
            return contentImages.stream().limit(3).collect(Collectors.toList());
        }
        return contentImages.stream().limit(1).collect(Collectors.toList());
    }
}
